#include <assert.h>
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>

#include <fcntl.h>
#include <unistd.h>

#include <ogg/ogg.h>
#include <vorbis/codec.h>
#include <vorbis/vorbisenc.h>

#include "../au.h"
#include "../common.h"
#include "afile.h"

enum {
	A_NULL,
	A_SIZE,
	A_ENC,
	A_RATE,
	A_CHAN,
	A_QUALITY
};

struct afile {
	vorbis_info	 i;
	vorbis_comment	 c;
	vorbis_dsp_state d;
	vorbis_block	 b;
	ogg_packet	 p;
	ogg_page	 g;
	ogg_stream_state s;
	size_t		 o;
	float		 quality;
	int		 fd;
	int		 first;
	int		 n;
};

static int	ov_writeout(struct afile *, size_t);
static int	ov_writehdr(struct afile *);
static int	ov_errno(int);

const struct a_lookup stab[] = {
	{ "size", A_SIZE },
	{ "enc", A_ENC },
	{ "encoding", A_ENC },
	{ "rate", A_RATE },
	{ "chan", A_CHAN },
	{ "channel", A_CHAN },
	{ "channels", A_CHAN },
	{ "quality", A_QUALITY },
	{ NULL, A_NULL }
};

struct afile *
af_open(const char *file)
{
	int fd;

	if (file == NULL)
		ERR(EINVAL, NULL);
	if ((fd = open(file, O_WRONLY)) < 0)
		return (NULL);
	return (af_fdopen(fd));
}

struct afile *
af_fdopen(int fd)
{
	struct afile *a;

	if (fd < 0)
		ERR(EINVAL, NULL);
	if ((a = malloc(sizeof(struct afile))) == NULL)
		return (NULL);
	a->fd = fd;
	a->first = 1;
	a->n = 0;
	a->quality = 1;
	vorbis_info_init(&a->i);
	vorbis_comment_init(&a->c);
	return (a);
}

void
af_close(struct afile *a)
{

	if (a == NULL)
		return;
	if (!a->first) {
		(void)ov_writeout(a, 0);
		ogg_stream_clear(&a->s);
		vorbis_block_clear(&a->b);
		vorbis_dsp_clear(&a->d);
	}
	vorbis_comment_clear(&a->c);
	vorbis_info_clear(&a->i);
	close(a->fd);
	free(a);
}

int
af_get(struct afile *a, const char *s, ...)
{
	va_list ap;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_SIZE:
			goto err;
		case A_ENC:
			*va_arg(ap, uint32_t *) = AU_FLOAT32;
			break;
		case A_RATE:
			*va_arg(ap, uint32_t *) = a->i.rate;
			break;
		case A_CHAN:
			*va_arg(ap, uint32_t *) = a->i.channels;
			break;
		case A_QUALITY:
			*va_arg(ap, double *) = a->quality;
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

int
af_set(struct afile *a, const char *s, ...)
{
	va_list ap;
	uint32_t e;
	ssize_t r;
	int n;
	int v;

	if (a == NULL || s == NULL)
		ERR(EINVAL, -1);
	va_start(ap, s);
	for (n = 0; (r = a_lookup(stab, s, &v)) > 0; n++, s += r)
		switch (v) {
		case A_SIZE:
			goto err;
		case A_ENC:
			e = va_arg(ap, uint32_t);
			if (e != AU_FLOAT32)
				goto err;
			a->o = au_encsize(e);
			break;
		case A_RATE:
			a->i.rate = va_arg(ap, uint32_t);
			break;
		case A_CHAN:
			a->i.channels = va_arg(ap, uint32_t);
			break;
		case A_QUALITY:
			a->quality = va_arg(ap, double);
			break;
		default:
			goto err;
		}
	if (r == 0) {
		va_end(ap);
		return (n);
	}
err:
	va_end(ap);
	ERR(EINVAL, n);
}

ssize_t
af_write(struct afile *a, void *b, size_t size)
{
	const float *fp;
	float **fb;
	size_t chan;
	size_t i;
	size_t n;
	size_t vals;

	if (a == NULL || b == NULL || a->fd < 0)
		ERR(EINVAL, -1);
	if (a->first) {
		if (ov_writehdr(a) != 0)
			return (-1);
		a->first = 0;
	}
	chan = a->i.channels;
	if ((vals = size / a->o / chan) > INT_MAX)
		ERR(ERANGE, -1);
	if ((fb = vorbis_analysis_buffer(&a->d, vals)) == NULL)
		return (-1);
	for (fp = b, n = 0; n < vals; n++)
		for (i = 0; i < chan; i++)
			fb[i][n] = fp[n * chan + i];
	for (i = 0; i < chan; i++)
		(void)au_encnswap(AU_FLOAT32, fb[i],
		    vals * sizeof(float));
	if (ov_writeout(a, vals) != 0)
		return (-1);
	return (size);
}

int
ov_writeout(struct afile *a, size_t vals)
{
	int e;

	if ((e = vorbis_analysis_wrote(&a->d, vals)) != 0)
		ERR(ov_errno(e), -1);
	while ((e = vorbis_analysis_blockout(&a->d, &a->b))
	    == 1) {
		if ((e = vorbis_analysis(&a->b, NULL)) != 0 ||
		    (e = vorbis_bitrate_addblock(&a->b)) != 0)
			ERR(ov_errno(e), -1);
		while ((e = vorbis_bitrate_flushpacket(&a->d,
		    &a->p)) == 1) {
			if (ogg_stream_packetin(&a->s, &a->p)
			    != 0)
				return (-1);
			while (ogg_stream_pageout(&a->s, &a->g)
			    != 0)
				if (nwrite(a->fd, a->g.header,
				    a->g.header_len) < 0 ||
				    nwrite(a->fd, a->g.body,
				    a->g.body_len) < 0)
					return (-1);
		}
	}
	if (e < 0)
		ERR(ov_errno(e), -1);
	return (0);
}

int
ov_writehdr(struct afile *a)
{
	ogg_packet p[3];
	int e;

	if ((e = vorbis_encode_init_vbr(&a->i, a->i.channels,
	    a->i.rate, a->quality)) != 0)
		ERR(ov_errno(e), -1);
	if (vorbis_analysis_init(&a->d, &a->i) != 0)
		return (-1);
	if (vorbis_block_init(&a->d, &a->b) != 0) {
		vorbis_dsp_clear(&a->d);
		return (-1);
	}
	if (ogg_stream_init(&a->s, a->n++) != 0) {
		vorbis_block_clear(&a->b);
		vorbis_dsp_clear(&a->d);
		return (-1);
	}
	if ((e = vorbis_analysis_headerout(&a->d, &a->c, &p[0],
	    &p[1], &p[2])) != 0) {
		errno = ov_errno(e);
		goto err;
	}
	if (ogg_stream_packetin(&a->s, &p[0]) != 0 ||
	    ogg_stream_packetin(&a->s, &p[1]) != 0 ||
	    ogg_stream_packetin(&a->s, &p[2]) != 0)
		goto err;
	while (ogg_stream_flush(&a->s, &a->g) != 0)
		if (nwrite(a->fd, a->g.header, a->g.header_len) < 0 ||
		    nwrite(a->fd, a->g.body, a->g.body_len) < 0)
			goto err;
	return (0);
err:
	ogg_stream_clear(&a->s);
	vorbis_block_clear(&a->b);
	vorbis_dsp_clear(&a->d);
	return (-1);
}

int
ov_errno(int e)
{

	switch (e) {
	case OV_EREAD:
		return (EIO);
	case OV_EFAULT:
		return (EFAULT);
	case OV_EIMPL:
		return (EOPNOTSUPP);
	case OV_EINVAL:
		return (EINVAL);
	case OV_ENOTVORBIS:
		return (EFTYPE);
	case OV_EBADHEADER:
		return (EFTYPE);
	case OV_EVERSION:
		return (EOPNOTSUPP);
	case OV_EBADLINK:
		return (EILSEQ);
	case OV_ENOSEEK:
		return (EOPNOTSUPP);
	default:
		return (EINVAL);
	}
}
